package action

import (
	"context"
	"net/http"
	"strings"

	"github.com/glennliao/apijson-go/config"
	"github.com/glennliao/apijson-go/consts"
	"github.com/glennliao/apijson-go/model"
	"github.com/glennliao/apijson-go/util"
	"github.com/gogf/gf/v2/errors/gerror"
	"github.com/samber/lo"
)

type J节点 struct {
	Q请求数据  []model.Map
	S上下文   context.Context
	S所属操作  *C操作
	J键名    string
	S是否为列表 bool
	B表名    string
	J角色    string

	S数据  []model.Map // 需写入数据库的数据
	T条件  []model.Map // 查询条件
	F返回值 model.Map   // 节点返回值
	Z主键  string      // 主键字段名

	J结构定义 *config.J结构定义
	Z执行器  string

	J节点映射 map[string]*J节点
}

func X新建节点(键名 string, 请求数据 []model.Map, 结构定义 *config.J结构定义, 执行器 string) J节点 {
	节点实例 := J节点{
		J键名:   键名,
		Q请求数据: 请求数据,
		J结构定义: 结构定义,
		Z执行器:  执行器,
	}

	节点实例.S数据 = []model.Map{}
	节点实例.T条件 = []model.Map{}

	for _ = range 节点实例.Q请求数据 {
		节点实例.S数据 = append(节点实例.S数据, model.Map{})
		节点实例.T条件 = append(节点实例.T条件, model.Map{})
	}

	if strings.HasSuffix(键名, consts.L列表键后缀) {
		节点实例.J键名 = util.Y移除后缀(键名, consts.L列表键后缀)
		节点实例.S是否为列表 = true
	}

	return 节点实例
}

// 解析请求数据到数据/条件中
func (节点实例 *J节点) J解析请求(方法 string) {
	for 索引, 项 := range 节点实例.Q请求数据 {
		for 字段, 值 := range 项 {
			if 字段 == consts.J角色 {
				节点实例.J角色 = util.Z转字符串(值)
				continue
			}

			字段 = 节点实例.S所属操作.S数据库字段风格(节点实例.S上下文, 节点实例.B表名, 字段)

			switch 方法 {
			case http.MethodPost:
				节点实例.S数据[索引][字段] = 值
			case http.MethodDelete:
				节点实例.T条件[索引][字段] = 值
			case http.MethodPut:
				if 字段 == 节点实例.Z主键 || 字段 == 节点实例.Z主键+consts.C操作符包含 {
					节点实例.T条件[索引][字段] = 值
				} else {
					节点实例.S数据[索引][字段] = 值
				}
			}
		}
	}
}

// 解析节点
func (节点实例 *J节点) J解析(上下文 context.Context, 方法 string) error {
	键名 := 节点实例.J键名
	if strings.HasSuffix(键名, consts.L列表键后缀) {
		键名 = util.Y移除后缀(键名, consts.L列表键后缀)
	}

	访问配置, 错误 := 节点实例.S所属操作.C操作配置.H获取访问配置(键名, true)
	if 错误 != nil {
		return 错误
	}

	节点实例.B表名 = 访问配置.M名称
	节点实例.Z主键 = 访问配置.Z主键字段

	// 1. 角色更新
	错误 = 节点实例.G更新角色()
	if 错误 != nil {
		return 错误
	}

	var 允许角色列表 []string
	if 节点实例.S所属操作.B不验证访问权限 == false {
		// 2. 检查权限
		switch 方法 {
		case http.MethodPost:
			允许角色列表 = 访问配置.T提交权限
		case http.MethodPut:
			允许角色列表 = 访问配置.G更新权限
		case http.MethodDelete:
			允许角色列表 = 访问配置.S删除权限
		}

		错误 = 节点实例.J检查权限(上下文, 方法, 允许角色列表)
		if 错误 != nil {
			return 错误
		}
	}

	// 3. 检查请求参数
	错误 = 节点实例.J检查请求()
	if 错误 != nil {
		return 错误
	}

	// 4. 根据访问条件更新条件
	错误 = 节点实例.G更新条件(上下文, 方法, 允许角色列表)

	节点实例.J解析请求(方法)

	return 错误
}

// 更新节点角色
func (节点实例 *J节点) G更新角色() error {
	if 值, 存在 := 节点实例.J结构定义.C插入字段[consts.J角色]; 存在 {
		if 节点实例.J角色 == "" {
			节点实例.J角色 = util.Z转字符串(值)
		}
	}

	if 值, 存在 := 节点实例.J结构定义.G更新字段[consts.J角色]; 存在 {
		节点实例.J角色 = util.Z转字符串(值)
	}

	return nil
}

// 检查节点是否有访问权限
func (节点实例 *J节点) J检查权限(ctx context.Context, method string, accessRoles []string) error {

	// 获取默认角色
	role, err := 节点实例.S所属操作.C操作配置.H获取默认角色函数()(ctx, config.M角色请求{
		F访问名称: 节点实例.B表名,
		Q请求方法: method,
		J节点角色: 节点实例.J角色,
	})

	// 如果获取默认角色失败，则返回错误
	if err != nil {
		return err
	}

	// 如果默认角色为DENY，则返回无权限错误
	if role == consts.J拒绝访问 {
		return consts.X新建拒绝访问错误(节点实例.J键名, 节点实例.J角色)
	}

	// 将默认角色赋值给节点角色
	节点实例.J角色 = role

	// 如果节点角色不在访问角色列表中，则返回无权限错误
	if !lo.Contains(accessRoles, role) {
		return consts.X新建无访问权限错误(节点实例.J键名, 节点实例.J角色)
	}

	// 返回nil，表示有权限
	return nil
}

func (节点实例 *J节点) G更新条件(上下文 context.Context, 方法 string, 允许角色列表 []string) error {
	for 索引, 项 := range 节点实例.Q请求数据 {
		条件结果 := config.X新建条件结果()
		请求参数 := model.Map{}

		for 字段, 值 := range 项 {
			字段 := 节点实例.S所属操作.S数据库字段风格(上下文, 节点实例.Z主键, 字段)
			请求参数[字段] = 值
		}

		条件请求 := config.T条件请求{
			F访问名称:    节点实例.J键名,
			B表访问角色列表: 允许角色列表,
			Q请求方法:    方法,
			J节点角色:    节点实例.J角色,
			J节点请求数据:  请求参数,
		}

		错误 := 节点实例.S所属操作.C操作配置.Z执行条件函数(上下文, 条件请求, 条件结果)
		if 错误 != nil {
			return 错误
		}

		if 方法 == http.MethodPost {
			for 字段, 值 := range 条件结果.H获取所有条件() {
				节点实例.S数据[索引][字段] = 值
			}
		} else {
			for 字段, 值 := range 条件结果.H获取所有条件() {
				节点实例.T条件[索引][字段] = 值
			}
		}
	}

	return nil
}
func (节点 *J节点) J检查请求() error {
	for _, 请求项 := range 节点.Q请求数据 {
		// 检查必填字段
		for _, 必填字段 := range 节点.J结构定义.B必填字段 {
			if _, 存在 := 请求项[必填字段]; !存在 {
				return consts.X新建结构键未找到错误(节点.J键名 + "." + 必填字段)
			}
		}

		// 检查禁止字段
		if len(节点.J结构定义.J拒绝字段) > 0 && 节点.J结构定义.J拒绝字段[0] == "!" {
			if len(节点.J结构定义.B必填字段) == 0 {
				return consts.X新建结构验证错误("REFUSE为!时必须指定MUST:" + 节点.J键名)
			}

			for 字段, _ := range 请求项 {
				if !lo.Contains(节点.J结构定义.B必填字段, 字段) {
					return consts.X新建结构验证错误("不能包含:" + 节点.J键名 + "." + 字段)
				}
			}

		} else {
			for _, 禁止字段 := range 节点.J结构定义.J拒绝字段 {
				if _, 存在 := 请求项[禁止字段]; 存在 {
					return consts.X新建结构验证错误("不能包含:" + 节点.J键名 + "." + 禁止字段)
				}
			}
		}
	}

	return nil
}

// 更新请求数据 处理 Update/Insert等
func (节点 *J节点) G更新请求数据() error {
	for 索引, _ := range 节点.Q请求数据 {
		for 字段, 更新值 := range 节点.J结构定义.G更新字段 {
			if strings.HasSuffix(字段, consts.H函数键后缀) {
				字段名 := 字段[0 : len(字段)-2]

				// 调用函数
				操作配置 := 节点.S所属操作.C操作配置
				函数名, 参数键 := util.J解析函数字符串(更新值.(string))
				函数 := 操作配置.H获取函数(函数名)

				参数 := model.Map{}
				for 参数索引, 参数项 := range 函数.C参数列表 {
					if 参数项.M名称 == consts.H函数原始请求参数 {
						参数[参数项.M名称] = 节点.S数据[索引]
					} else {
						参数[参数项.M名称] = 节点.S数据[索引][参数键[参数索引]]
					}
				}

				返回值, 错误 := 操作配置.T调用函数(节点.S上下文, 函数名, 参数)
				if 错误 != nil {
					return 错误
				}
				if 返回值 != nil {
					节点.S数据[索引][字段名] = 返回值
				}

			} else {
				节点.S数据[索引][字段] = 更新值
			}
		}

		for 字段, 插入值 := range 节点.J结构定义.C插入字段 {
			if _, 存在 := 节点.S数据[索引][字段]; !存在 {
				节点.S数据[索引][字段] = 插入值
			}
		}
	}

	return nil
}

// 执行前更新请求数据 (事务内)
func (节点 *J节点) Z执行前更新请求() error {
	for 索引, _ := range 节点.Q请求数据 {
		for 字段, 值 := range 节点.S数据[索引] {
			// 处理引用
			if strings.HasSuffix(字段, consts.Y引用键后缀) {
				引用节点键, 引用列 := util.X解析引用列(值.(string))
				if strings.HasSuffix(引用节点键, consts.L列表键后缀) { // 双列表
					节点.S数据[索引][字段] = 节点.J节点映射[引用节点键].S数据[索引][节点.S所属操作.S数据库字段风格(节点.S上下文, 节点.B表名, 引用列)]
				} else {
					节点.S数据[索引][字段] = 节点.J节点映射[引用节点键].S数据[0][节点.S所属操作.S数据库字段风格(节点.S上下文, 节点.B表名, 引用列)]
				}
			}
		}
	}

	return nil
}

func (节点 *J节点) Z执行操作(ctx context.Context, method string) (ret model.Map, err error) {

	var rowKeyVal model.Map
	var rowKey string

	access, err := 节点.S所属操作.C操作配置.H获取访问配置(节点.J键名, true)
	if err != nil {
		return nil, err
	}

	switch method {
	case http.MethodPost:

		if access.Z主键生成策略 != "" {
			for i, _ := range 节点.S数据 {

				rowKeyVal, err = 节点.S所属操作.C操作配置.S生成主键(ctx, access.Z主键生成策略, 节点.J键名, 节点.B表名, 节点.S数据[i])
				if err != nil {
					return nil, gerror.Wrap(err, "RowKeyGen")
				}

				for k, v := range rowKeyVal {
					if k == consts.H行键 {
						节点.S数据[i][access.Z主键字段] = v
					} else {
						节点.S数据[i][k] = v
					}
				}

			}
		}

		rowKey = access.Z主键字段

	case http.MethodPut:
	case http.MethodDelete:

	default:
		return nil, consts.X新建方法不支持错误(method)
	}

	executor, err := H获取操作执行器(节点.Z执行器)
	if err != nil {
		return nil, err
	}

	ret, err = executor.Z执行(ctx, C操作执行请求{
		F方法:     method,
		B表名:     节点.B表名,
		S数据:     节点.S数据,
		T条件:     节点.T条件,
		F访问配置:   access,
		C操作配置:   节点.S所属操作.C操作配置,
		X新建查询函数: 节点.S所属操作.X新建查询,
	})

	if err != nil {
		return nil, err
	}

	if len(节点.S数据) == 1 {

		jsonStyle := 节点.S所属操作.JSON字段风格
		if rowKeyVal != nil {
			for k, v := range rowKeyVal {
				if k == consts.H行键 {
					ret[jsonStyle(ctx, 节点.B表名, rowKey)] = v
				} else {
					ret[jsonStyle(ctx, 节点.B表名, k)] = v
				}
			}
		}
	}

	节点.F返回值 = ret

	return
}

func (节点 *J节点) Z执行(上下文 context.Context, 方法 string) (model.Map, error) {
	错误 := 节点.Z执行前更新请求()
	if 错误 != nil {
		return nil, 错误
	}

	返回结果, 错误 := 节点.Z执行操作(上下文, 方法)
	if 错误 != nil {
		return nil, 错误
	}

	节点.F返回值 = 返回结果
	return 节点.F返回值, nil
}
